/*
 * Copyright (C) 2011-2015 GUIGUI Simon, fyhertz@gmail.com
 *
 * This file is part of libstreaming (https://github.com/fyhertz/libstreaming)
 *
 * Spydroid is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * This source code is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this source code; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package net.majorkernelpanic.screening;

import net.majorkernelpanic.screening.audio.AudioStream;
import net.majorkernelpanic.screening.rtp.AbstractPacketizer;
import net.majorkernelpanic.screening.video.VideoStream;

import android.annotation.SuppressLint;
import android.media.MediaCodec;
import android.media.MediaRecorder;
import android.net.LocalServerSocket;
import android.net.LocalSocket;
import android.net.LocalSocketAddress;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.util.Log;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetAddress;
import java.util.Random;

/**
 * A MediaRecorder that streams what it records using a packetizer from the RTP package.
 * You can't use this class directly !
 */
public abstract class MediaStream implements Stream {
  protected static final String TAG = "MediaStream";

  /** Raw audio/video will be encoded using the MediaRecorder API. */
  public static final byte MODE_MEDIARECORDER_API = 0x01;

  /** Raw audio/video will be encoded using the MediaCode API with a surface. */
  public static final byte MODE_MEDIACODEC_API = 0x02;

  /** A LocalSocket will be used to feed the MediaRecorder object */
  public static final byte PIPE_API_LS = 0x01;

  /** A ParcelFileDescriptor will be used to feed the MediaRecorder object */
  public static final byte PIPE_API_PFD = 0x02;

  /** Prefix that will be used for all shared preferences saved by libscreening */
  protected static final String PREF_PREFIX = "libscreening-";

  /** The packetizer that will read data from an InputStream and send RTP packets over the networked. */
  protected AbstractPacketizer mPacketizer = null;

  protected static byte sSuggestedMode = MODE_MEDIARECORDER_API;
  protected byte mMode, mRequestedMode;

  /**
   * Starting lollipop the LocalSocket API cannot be used to feed a MediaRecorder object.
   * You can force what API to use to create the pipe that feeds it with this constant
   * by using  {@link #PIPE_API_LS} and {@link #PIPE_API_PFD}.
   */
  protected final static byte sPipeApi;

  protected boolean mStreaming = false, mConfigured = false;
  protected int mRtpPort = 0, mRtcpPort = 0;
  protected byte mChannelIdentifier = 0;
  protected OutputStream mOutputStream = null;
  protected InetAddress mDestination;

  protected ParcelFileDescriptor[] mParcelFileDescriptors;
  protected ParcelFileDescriptor mParcelRead;
  protected ParcelFileDescriptor mParcelWrite;

  protected LocalSocket mReceiver, mSender = null;
  private LocalServerSocket mLss = null;
  private int mSocketId;

  private int mTTL = 64;

  protected MediaRecorder mMediaRecorder;
  protected MediaCodec mMediaCodec;

  static {
    // We determine whether or not the MediaCodec API should be used
    try {
      Class.forName("android.media.MediaCodec");
      // Will be set to MODE_MEDIACODEC_API at some point...
      sSuggestedMode = MODE_MEDIACODEC_API;
      Log.i(TAG,"Phone supports the MediaCoded API");
    } catch (ClassNotFoundException e) {
      sSuggestedMode = MODE_MEDIARECORDER_API;
      Log.i(TAG,"Phone does not support the MediaCodec API");
    }

    // Starting lollipop, the LocalSocket API cannot be used anymore to feed
    // a MediaRecorder object for security reasons
    if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT_WATCH) {
      sPipeApi = PIPE_API_PFD;
    } else {
      sPipeApi = PIPE_API_LS;
    }
  }

  protected static File CACHE_DIR = null;

  public static void init(File dir) {
    CACHE_DIR = dir;
  }

  public MediaStream() {
    mRequestedMode = sSuggestedMode;
    mMode = sSuggestedMode;
  }

  /**
   * Sets the destination IP address of the stream.
   * @param dest The destination address of the stream
   */
  public void setDestinationAddress(InetAddress dest) {
    mDestination = dest;
  }

  /**
   * Sets the destination ports of the stream.
   * If an odd number is supplied for the destination port then the next
   * lower even number will be used for RTP and it will be used for RTCP.
   * If an even number is supplied, it will be used for RTP and the next odd
   * number will be used for RTCP.
   * @param dport The destination port
   */
  public void setDestinationPorts(int dport) {
    if (dport % 2 == 1) {
      mRtpPort = dport-1;
      mRtcpPort = dport;
    } else {
      mRtpPort = dport;
      mRtcpPort = dport+1;
    }
  }

  /**
   * Sets the destination ports of the stream.
   * @param rtpPort Destination port that will be used for RTP
   * @param rtcpPort Destination port that will be used for RTCP
   */
  public void setDestinationPorts(int rtpPort, int rtcpPort) {
    mRtpPort = rtpPort;
    mRtcpPort = rtcpPort;
    mOutputStream = null;
  }

  /**
   * If a TCP is used as the transport protocol for the RTP session,
   * the output stream to which RTP packets will be written to must
   * be specified with this method.
   */
  public void setOutputStream(OutputStream stream, byte channelIdentifier) {
    mOutputStream = stream;
    mChannelIdentifier = channelIdentifier;
  }

  /**
   * Sets the Time To Live of packets sent over the network.
   * @param ttl The time to live
   * @throws IOException
   */
  public void setTimeToLive(int ttl) throws IOException {
    mTTL = ttl;
  }

  /**
   * Returns a pair of destination ports, the first one is the
   * one used for RTP and the second one is used for RTCP.
   **/
  public int[] getDestinationPorts() {
    return new int[] {
        mRtpPort,
        mRtcpPort
    };
  }

  /**
   * Returns a pair of source ports, the first one is the
   * one used for RTP and the second one is used for RTCP.
   **/
  public int[] getLocalPorts() {
    return mPacketizer.getRtpSocket().getLocalPorts();
  }

  /**
   * Sets the streaming method that will be used.
   *
   * If the mode is set to {@link #MODE_MEDIARECORDER_API}, raw audio/video will be encoded
   * using the MediaRecorder API. <br />
   *
   * If the mode is set to {@link #MODE_MEDIACODEC_API},
   * audio/video will be encoded with using the MediaCodec. <br />
   *
   * @param mode Can be {@link #MODE_MEDIARECORDER_API}, {@link #MODE_MEDIACODEC_API}
   */
  public void setStreamingMethod(byte mode) {
    mRequestedMode = mode;
  }

  /**
   * Returns the streaming method in use, call this after
   * {@link #configure()} to get an accurate response.
   */
  public byte getStreamingMethod() {
    return mMode;
  }

  /**
   * Returns the packetizer associated with the {@link MediaStream}.
   * @return The packetizer
   */
  public AbstractPacketizer getPacketizer() {
    return mPacketizer;
  }

  /**
   * Returns an approximation of the bit rate consumed by the stream in bit per seconde.
   */
  public long getBitrate() {
    return !mStreaming ? 0 : mPacketizer.getRtpSocket().getBitrate();
  }

  /**
   * Indicates if the {@link MediaStream} is streaming.
   * @return A boolean indicating if the {@link MediaStream} is streaming
   */
  public boolean isStreaming() {
    return mStreaming;
  }

  /**
   * Configures the stream with the settings supplied with
   * {@link VideoStream#setVideoQuality(net.majorkernelpanic.screening.video.VideoQuality)}
   * for a {@link VideoStream} and {@link AudioStream#setAudioQuality(net.majorkernelpanic.screening.audio.AudioQuality)}
   * for a {@link AudioStream}.
   */
  public synchronized void configure() throws IllegalStateException, IOException {
    if (mStreaming) throw new IllegalStateException("Can't be called while streaming.");
    if (mPacketizer != null) {
      mPacketizer.setDestination(mDestination, mRtpPort, mRtcpPort);
      mPacketizer.getRtpSocket().setOutputStream(mOutputStream, mChannelIdentifier);
    }
    mMode = mRequestedMode;
    mConfigured = true;
  }

  /** Starts the stream. */
  public synchronized void start() throws IllegalStateException, IOException {
    if (!mStreaming) {
      if (mDestination==null)
        throw new IllegalStateException("No destination ip address set for the stream !");

      if (mRtpPort<=0 || mRtcpPort<=0)
        throw new IllegalStateException("No destination ports set for the stream !");

      mPacketizer.setTimeToLive(mTTL);

      if (mMode != MODE_MEDIARECORDER_API) {
        encodeWithMediaCodec();
      } else {
        encodeWithMediaRecorder();
      }

      mStreaming = true;
    }
  }

  /** Stops the stream. */
  @SuppressLint("NewApi")
  public synchronized  void stop() {
    if (mStreaming) {
      try {
        if (mMode==MODE_MEDIARECORDER_API) {
          mMediaRecorder.stop();
          mMediaRecorder.release();
          mMediaRecorder = null;
          closeSockets();
          mPacketizer.stop();
        } else {
          mPacketizer.stop();
          mMediaCodec.stop();
          mMediaCodec.release();
          mMediaCodec = null;
        }
      } catch (Exception e) {
        e.printStackTrace();
      }
      mStreaming = false;
    }
  }

  protected abstract void encodeWithMediaRecorder() throws IOException;

  protected abstract void encodeWithMediaCodec() throws IOException;

  /**
   * Returns a description of the stream using SDP.
   * This method can only be called after {@link Stream#configure()}.
   * @throws IllegalStateException Thrown when {@link Stream#configure()} was not called.
   */
  public abstract String getSessionDescription();

  /**
   * Returns the SSRC of the underlying {@link net.majorkernelpanic.screening.rtp.RtpSocket}.
   * @return the SSRC of the stream
   */
  public int getSSRC() {
    return getPacketizer().getSSRC();
  }

  protected void createSockets() throws IOException {

    if (sPipeApi == PIPE_API_LS) {

      final String LOCAL_ADDR = "net.majorkernelpanic.screening-";

      for (int i=0;i<10;i++) {
        try {
          mSocketId = new Random().nextInt();
          mLss = new LocalServerSocket(LOCAL_ADDR+mSocketId);
          break;
        } catch (IOException e1) {}
      }

      mReceiver = new LocalSocket();
      mReceiver.connect( new LocalSocketAddress(LOCAL_ADDR+mSocketId));
      mReceiver.setReceiveBufferSize(500000);
      mReceiver.setSoTimeout(3000);
      mSender = mLss.accept();
      mSender.setSendBufferSize(500000);

    } else {
      Log.e(TAG, "parcelFileDescriptors createPipe version = Lollipop");
      mParcelFileDescriptors = ParcelFileDescriptor.createPipe();
      mParcelRead = new ParcelFileDescriptor(mParcelFileDescriptors[0]);
      mParcelWrite = new ParcelFileDescriptor(mParcelFileDescriptors[1]);
    }
  }

  protected void closeSockets() {
    if (sPipeApi == PIPE_API_LS) {
      try {
        mReceiver.close();
      } catch (Exception e) {
        e.printStackTrace();
      }
      try {
        mSender.close();
      } catch (Exception e) {
        e.printStackTrace();
      }
      try {
        mLss.close();
      } catch (Exception e) {
        e.printStackTrace();
      }
      mLss = null;
      mSender = null;
      mReceiver = null;

    } else {
      try {
        if (mParcelRead != null) {
          mParcelRead.close();
        }
      } catch (Exception e) {
        e.printStackTrace();
      }
      try {
        if (mParcelWrite != null) {
          mParcelWrite.close();
        }
      } catch (Exception e) {
        e.printStackTrace();
      }
    }
  }
}
